Skip to content

Plugin Management API ​

This document is based on the following source files:

  • internal/handlers/jsplugin.go -- JS plugin management handler (CRUD, enable/disable, update)
  • internal/handlers/jsplugin_registry.go -- Plugin registry sources and registry installation
  • internal/jsplugin/routes.go -- JS plugin runtime routes (static resources, API forwarding, file access)
  • internal/app/routers.go -- Route registration

Table of Contents ​

  1. Overview
  2. Plugin Management Endpoints
  3. Plugin Update Endpoints
  4. Registry Endpoints
  5. Source Health
  6. Plugin Settings Endpoints
  7. Plugin Runtime Routes

1. Overview ​

The JS plugin system is Songloft's extension mechanism, running on a QuickJS sandbox. The plugin management API is divided into three layers:

  • Management endpoints (/api/v1/jsplugins/*): plugin installation, uninstallation, enable/disable, and update; require JWT authentication
  • Settings endpoints (/api/v1/settings/*): global configuration such as registry sources and proxy; require JWT authentication
  • Runtime routes (/api/v1/jsplugin/{entryPath}/*): a plugin's static resource serving and API forwarding; authentication rules depend on whether publicPaths is declared

Management endpoints are registered by JSPluginHandler.RegisterRoutes, and runtime routes are registered by Manager.RegisterStaticRoutes / RegisterAPIRoutes.


2. Plugin Management Endpoints ​

2.1 GET /jsplugins -- List All Plugins ​

Method: GETPath: /api/v1/jspluginsAuthentication: BearerAuth required

Description: Gets the list of all installed JS plugins.

Success response (200):

json
{
  "plugins": [
    {
      "id": 1,
      "name": "示例插件",
      "entry_path": "example-plugin",
      "version": "1.0.0",
      "status": "active",
      "icon": "icon.png",
      ...
    }
  ]
}

2.2 POST /jsplugins/upload -- Upload and Install Plugin ​

Method: POSTPath: /api/v1/jsplugins/uploadAuthentication: BearerAuth required

Description: Uploads a .jsplugin.zip archive to install a new plugin. If the entry_path already exists, it automatically follows the overwrite-update path and hot-reloads the plugin if it is active. The upload size limit is 50MB.

Request: multipart/form-data

FieldTypeRequiredDescription
filefileYesJS plugin file (.jsplugin.zip)

Success response (201 new install / 200 update):

json
{
  "total": 1,
  "success": 1,
  "failed": 0,
  "results": [
    {
      "file_name": "example.jsplugin.zip",
      "plugin": { ... },
      "success": true
    }
  ],
  "message": "插件 example-plugin 安装成功"
}

On installation failure (200):

json
{
  "total": 1,
  "success": 0,
  "failed": 1,
  "results": [
    {
      "file_name": "bad.zip",
      "error": "manifest 缺失",
      "success": false
    }
  ],
  "message": "安装插件失败"
}

2.3 GET /jsplugins/{id} -- Get Plugin Details ​

Method: GETPath: /api/v1/jsplugins/{id}Authentication: BearerAuth required

Path parameter:

ParameterTypeDescription
idintPlugin ID

Success response (200):

json
{
  "plugin": {
    "id": 1,
    "name": "示例插件",
    "entry_path": "example-plugin",
    "version": "1.0.0",
    "status": "active",
    ...
  }
}

Error responses:

StatusDescription
404Plugin does not exist

2.4 PUT /jsplugins/{id} -- Update Plugin ​

Method: PUTPath: /api/v1/jsplugins/{id}Authentication: BearerAuth required

Description: Uploads a new .jsplugin.zip file to update an existing plugin. Plugins in the active state are automatically reloaded after the update.

Request: multipart/form-data

FieldTypeRequiredDescription
filefileYesJS plugin file (.jsplugin.zip)

Path parameter:

ParameterTypeDescription
idintPlugin ID

Success response (200):

json
{
  "plugin": { ... }
}

Error responses:

StatusDescription
400Bad request data or update failed
404Plugin does not exist

2.5 DELETE /jsplugins/{id} -- Delete Plugin ​

Method: DELETEPath: /api/v1/jsplugins/{id}Authentication: BearerAuth required

Description: Deletes the specified plugin. First uninstalls the running service, then deletes the files and database record, and finally refreshes the publicPaths cache.

Path parameter:

ParameterTypeDescription
idintPlugin ID

Success response (200):

json
{
  "message": "插件已删除"
}

Error responses:

StatusDescription
404Plugin does not exist
500Failed to delete plugin

2.6 POST /jsplugins/{id}/enable -- Enable Plugin ​

Method: POSTPath: /api/v1/jsplugins/{id}/enableAuthentication: BearerAuth required

Description: Enables the specified JS plugin, loading the JS runtime.

Path parameter:

ParameterTypeDescription
idintPlugin ID

Success response (200):

json
{
  "plugin": { ... }
}

2.7 POST /jsplugins/{id}/disable -- Disable Plugin ​

Method: POSTPath: /api/v1/jsplugins/{id}/disableAuthentication: BearerAuth required

Description: Disables the specified JS plugin, unloading the JS runtime.

Path parameter:

ParameterTypeDescription
idintPlugin ID

Success response (200):

json
{
  "plugin": { ... }
}

3. Plugin Update Endpoints ​

3.1 GET /jsplugins/{id}/check-update -- Check a Single Plugin for Updates ​

Method: GETPath: /api/v1/jsplugins/{id}/check-updateAuthentication: BearerAuth required

Description: Checks whether the specified plugin has a remote update.

Path parameter:

ParameterTypeDescription
idintPlugin ID

Query parameters:

ParameterTypeRequiredDescription
github_proxystringNoGitHub proxy prefix

Success response (200):

json
{
  "has_update": true,
  "current_version": "1.0.0",
  "remote_version": "1.1.0",
  "download_url": "https://github.com/.../releases/download/v1.1.0/plugin.zip"
}

3.2 POST /jsplugins/{id}/update -- Download and Update a Single Plugin ​

Method: POSTPath: /api/v1/jsplugins/{id}/updateAuthentication: BearerAuth required

Description: Downloads and updates the specified plugin from the remote. Plugins in the active state are automatically reloaded after the update.

Path parameter:

ParameterTypeDescription
idintPlugin ID

Request body (optional):

json
{
  "github_proxy": "",
  "force": false
}
FieldTypeDescription
github_proxystringGitHub proxy prefix
forcebooleanSet to true to skip the version check and force a re-download and reinstall

Success response (200):

json
{
  "plugin": { ... }
}

3.3 POST /jsplugins/update-all -- Batch Update All Plugins ​

Method: POSTPath: /api/v1/jsplugins/update-allAuthentication: BearerAuth required

Description: Checks and updates all plugins that have a remote update source. Skips plugins without an update_url and plugins already on the latest version. Downloads and installs updates one by one; a single failure does not interrupt the update flow of other plugins.

Request body (optional):

json
{
  "github_proxy": "",
  "force": false
}
FieldTypeDescription
github_proxystringGitHub proxy prefix
forcebooleanSet to true to skip the version check and force a re-download and reinstall of all plugins

Success response (200):

json
{
  "total": 5,
  "updated": 2,
  "failed": 1,
  "skipped": 2,
  "results": [
    {
      "plugin_id": 1,
      "plugin_name": "Plugin A",
      "entry_path": "plugin-a",
      "success": true,
      "has_update": true,
      "current_version": "1.0.0",
      "new_version": "1.1.0"
    },
    {
      "plugin_id": 2,
      "plugin_name": "Plugin B",
      "entry_path": "plugin-b",
      "success": false,
      "has_update": true,
      "current_version": "2.0.0",
      "error": "下载更新失败: http status 404"
    }
  ],
  "message": "批量更新完成:2 已更新,1 失败,2 无需更新"
}

4. Registry Endpoints ​

Flutter/Lynx normally fetch store metadata progressively from the client; the refresh endpoint below remains a compatible fallback for Web CORS or connection failures. Reads and installation downloads with source tokens bypass GitHub acceleration proxies. Authenticated GET/PUT /api/v1/settings/github-discovery-token reads/writes { "token": "..." } (an empty string clears it; responses use Cache-Control: no-store). The shared token is only for direct client requests to the official API; source tokens win, and server fallback and installation do not use the shared token.

4.1 POST /jsplugins/registry/refresh -- Refresh Registry ​

Method: POSTPath: /api/v1/jsplugins/registry/refreshAuthentication: BearerAuth required

Description: Fetches the specified registry source URL (including recursive includes), deduplicates and merges, then returns a paginated list of available plugins. Each plugin is marked with whether it is installed and whether it has an update.

Request body:

json
{
  "registry_url": "https://raw.githubusercontent.com/.../registry.json",
  "page": 1,
  "page_size": 20,
  "search": "",
  "github_proxy": ""
}
FieldTypeRequiredDescription
registry_urlstringYesRegistry source URL
pageintNoPage number (default 1)
page_sizeintNoItems per page (default 20, maximum 100)
searchstringNoSearch keyword (matches name, description, author, entry_path)
github_proxystringNoGitHub proxy prefix

Success response (200):

json
{
  "plugins": [
    {
      "name": "MiOT 音源",
      "entry_path": "songloft-plugin-miot",
      "version": "1.2.0",
      "description": "IoT 智能音箱音源",
      "author": "Songloft",
      "homepage": "https://github.com/...",
      "icon": "icon.png",
      "download_url": "https://github.com/.../releases/download/v1.2.0/plugin.zip",
      "installed": true,
      "installed_version": "1.1.0",
      "has_update": true
    }
  ],
  "total": 15,
  "page": 1,
  "page_size": 20,
  "warnings": []
}

4.2 POST /jsplugins/registry/install -- Install Plugin from Registry ​

Method: POSTPath: /api/v1/jsplugins/registry/installAuthentication: BearerAuth required

Description: Downloads the ZIP from the download_url in the registry and installs the plugin. If the entry_path already exists, it automatically follows the update path. Supports a GitHub proxy. The ZIP download size limit is 50MB.

Request body:

json
{
  "download_url": "https://github.com/.../releases/download/v1.0.0/plugin.zip",
  "github_proxy": ""
}
FieldTypeRequiredDescription
download_urlstringYesPlugin ZIP download address
github_proxystringNoGitHub proxy prefix (prepended to download_url)

Success response (201 new install / 200 update):

The response format is the same as POST /jsplugins/upload (jsPluginUploadResponse).


5. Source Health ​

5.1 GET /plugins/health -- Plugin Health ​

Method: GETPath: /api/v1/plugins/healthAuthentication: BearerAuth required

Description: Returns the download success rate, health category, and most recent failure reasons for each music source plugin, to help troubleshoot plugin issues.

Success response (200):

json
{
  "plugins": [
    {
      "entry_path": "songloft-plugin-miot",
      "name": "MiOT 音源",
      "total_requests": 100,
      "success_count": 95,
      "failure_count": 5,
      "success_rate": 0.95,
      "health": "green",
      "recent_failures": [
        {
          "error": "connection timeout",
          "time": "2026-06-12T10:00:00Z"
        }
      ]
    }
  ]
}

6. Plugin Settings Endpoints ​

6.1 GET/PUT /settings/plugin-registries -- Registry List Configuration ​

Path: /api/v1/settings/plugin-registriesAuthentication: BearerAuth required

Manages the list of plugin registry source URLs configured by the user.

GET response / PUT request body: ​

json
{
  "registries": [
    {
      "url": "https://raw.githubusercontent.com/songloft-org/songloft-plugin-registry/main/registry.json",
      "name": "Songloft 官方插件",
      "enabled": true
    }
  ]
}
FieldTypeDescription
registries[].urlstringRegistry source URL
registries[].namestringRegistry source name
registries[].enabledbooleanWhether it is enabled

When not configured, GET returns the built-in default value (the Songloft official plugin source).


6.2 GET/PUT /settings/http-proxy -- HTTP Proxy Configuration ​

Path: /api/v1/settings/http-proxyAuthentication: BearerAuth required Tag: Settings

Configures the global HTTP proxy address. All outbound backend requests (plugin downloads, registry fetching, upgrade checks, etc.) are forwarded through this proxy.

GET response / PUT request body: ​

json
{
  "proxy": "http://192.168.1.1:7890"
}
FieldTypeDescription
proxystringProxy address (supports HTTP/HTTPS/SOCKS5); an empty string means a direct connection

Takes effect immediately after saving, no restart required. Loopback addresses (localhost/127.0.0.1/::1) automatically skip the proxy.

PUT error responses:

StatusDescription
400Bad request format or invalid proxy address
500Failed to save configuration

7. Plugin Runtime Routes ​

The following routes are registered by Manager.RegisterStaticRoutes and Manager.RegisterAPIRoutes, serving a plugin's frontend pages and API calls. {entryPath} is determined at runtime by the installed plugins; the OpenAPI schema is a placeholder only.

7.1 GET /jsplugin/{entryPath} -- Plugin Root Page ​

Method: GETPath: /api/v1/jsplugin/{entryPath} and /api/v1/jsplugin/{entryPath}/Authentication: None required

Description: The JS plugin entry HTML. Injects the <base> tag (so relative paths resolve correctly) and the auth-bridge script, then returns static/index.html. Static file serving does not depend on whether the JS runtime is ready, ensuring the page can still load during plugin initialization.

Success response (200): HTML page Error response (404): Plugin not installed or missing static/index.html


7.2 GET /jsplugin/{entryPath}/static/* -- Plugin Static Resources ​

Method: GETPath: /api/v1/jsplugin/{entryPath}/static and /api/v1/jsplugin/{entryPath}/static/*Authentication: None required

Description: Returns static resources such as CSS/JS/images from the plugin's disk directory. Paths that do not match SPA-fall back to index.html. HTML files inject the <base> tag and are set to no-cache; other resources are set to strong caching (1 year).


7.3 ANY /jsplugin/{entryPath}/* -- Plugin API Forwarding ​

Method: GET / POST / PUT / DELETE (catch-all) Path: /api/v1/jsplugin/{entryPath}/*Authentication: BearerAuth required (except for paths declared in publicPaths)

Description: Accepts any HTTP method, dispatching to the plugin's static fallback or forwarding to the plugin code in the QuickJS sandbox. Non-static paths trigger on-demand lazy loading -- the first request after idle eviction automatically reloads the plugin.

The request body is passed to the JS runtime via JSON serialization; when the body contains non-UTF-8 bytes (such as a multipart upload), base64 encoding is used automatically for passthrough.

Error responses:

StatusError codeDescription
403plugin_disabledPlugin not enabled
404plugin_not_foundPlugin does not exist
503plugin_unavailablePlugin unavailable or running abnormally (the health check will self-heal)
504-JS runtime call timed out

The frontend automatically retries a 503 plugin_unavailable once (200ms delay), in coordination with the backend's lazy-loading/self-healing mechanism.


7.4 GET/HEAD /jsplugin/{entryPath}/files/* -- Plugin File Access ​

Method: GET / HEADPath: /api/v1/jsplugin/{entryPath}/files/*Authentication: BearerAuth required

Description: Returns files within the plugin's accessible scope directly via Go's native http.ServeFile, supporting Range requests and HTTP caching.

Path resolution rules:

Path formatDescriptionRequired permission
relative/pathRelative to the plugin's data directoryfs
/absolute/pathAbsolute path, validated to be within the configured directoryfs:external
music://xxxResolved as {music_path}/xxxfs:music

Security measures: filepath.Abs + HasPrefix prevents path traversal; paths containing .. are directly rejected.


7.5 GET /jsplugin-assets/* -- Plugin Common Resources ​

Method: GETPath: /api/v1/jsplugin-assets/*Authentication: None required

Description: Serves the plugin common CSS, JS, and font files embedded by the main program. injectHTMLHead automatically injects them into all plugin HTML pages.

Included resources:

  • common.css -- Defines the --md-* CSS variables (light/dark dual themes)
  • common.js -- embed detection + theme bridging (postMessage real-time updates + data-theme attribute), exposes the window.SongloftPlugin global API

Resources are set to strong caching (1 year, immutable).


Section source: internal/handlers/jsplugin.go / internal/handlers/jsplugin_registry.go / internal/jsplugin/routes.go / internal/app/routers.go